/*
 * This file is part of aion-unique <aion-unique.org>.
 *
 *  aion-unique is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  aion-unique is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with aion-unique.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionemu.commons.utils.concurrent;

import java.io.PrintStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import javolution.text.TextBuilder;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.log4j.Logger;

/**
 * @author NB4L1
 */
@SuppressWarnings("unchecked")
public final class RunnableStatsManager
{
	/**
	 * Logger for this class
	 */
	private static final Logger						log			= Logger.getLogger(RunnableStatsManager.class);

	private static final Map<Class<?>, ClassStat>	classStats	= new HashMap<Class<?>, ClassStat>();

	private static final class ClassStat
	{
		private final String		className;
		private final MethodStat	runnableStat;

		private String[]			methodNames	= new String[0];
		private MethodStat[]		methodStats	= new MethodStat[0];

		private ClassStat(Class<?> clazz)
		{
			className = clazz.getName().replace("com.aionemu.gameserver.", "");
			runnableStat = new MethodStat(className, "run()");

			methodNames = new String[] { "run()" };
			methodStats = new MethodStat[] { runnableStat };

			classStats.put(clazz, this);
		}

		private MethodStat getRunnableStat()
		{
			return runnableStat;
		}

		private MethodStat getMethodStat(String methodName, boolean synchronizedAlready)
		{
			// method names will be interned automatically because of compiling, so this gonna work
			if(methodName == "run()")
				return runnableStat;

			for(int i = 0; i < methodNames.length; i++)
				if(methodNames[i].equals(methodName))
					return methodStats[i];

			if(!synchronizedAlready)
			{
				synchronized(this)
				{
					return getMethodStat(methodName, true);
				}
			}

			methodName = methodName.intern();

			final MethodStat methodStat = new MethodStat(className, methodName);

			methodNames = (String[]) ArrayUtils.add(methodNames, methodName);
			methodStats = (MethodStat[]) ArrayUtils.add(methodStats, methodStat);

			return methodStat;
		}
	}

	private static final class MethodStat
	{
		private final ReentrantLock	lock	= new ReentrantLock();

		private final String		className;
		private final String		methodName;

		private long				count;
		private long				total;
		private long				min		= Long.MAX_VALUE;
		private long				max		= Long.MIN_VALUE;

		private MethodStat(String className, String methodName)
		{
			this.className = className;
			this.methodName = methodName;
		}

		private void handleStats(long runTime)
		{
			lock.lock();
			try
			{
				count++;
				total += runTime;
				min = Math.min(min, runTime);
				max = Math.max(max, runTime);
			}
			finally
			{
				lock.unlock();
			}
		}
	}

	private static ClassStat getClassStat(Class<?> clazz, boolean synchronizedAlready)
	{
		ClassStat classStat = classStats.get(clazz);

		if(classStat != null)
			return classStat;

		if(!synchronizedAlready)
		{
			synchronized(RunnableStatsManager.class)
			{
				return getClassStat(clazz, true);
			}
		}

		return new ClassStat(clazz);
	}

	public static void handleStats(Class<? extends Runnable> clazz, long runTime)
	{
		getClassStat(clazz, false).getRunnableStat().handleStats(runTime);
	}

	public static void handleStats(Class<?> clazz, String methodName, long runTime)
	{
		getClassStat(clazz, false).getMethodStat(methodName, false).handleStats(runTime);
	}

	public static enum SortBy
	{
		AVG("average"),
		COUNT("count"),
		TOTAL("total"),
		NAME("class"),
		METHOD("method"),
		MIN("min"),
		MAX("max"), ;

		private final String	xmlAttributeName;

		private SortBy(String xmlAttributeName)
		{
			this.xmlAttributeName = xmlAttributeName;
		}

		private final Comparator<MethodStat>	comparator	= new Comparator<MethodStat>(){
																@SuppressWarnings("rawtypes")
																@Override
																public int compare(MethodStat o1, MethodStat o2)
																{
																	final Comparable c1 = getComparableValueOf(o1);
																	final Comparable c2 = getComparableValueOf(o2);

																	if(c1 instanceof Number)
																		return c2.compareTo(c1);

																	final String s1 = (String) c1;
																	final String s2 = (String) c2;

																	final int len1 = s1.length();
																	final int len2 = s2.length();
																	final int n = Math.min(len1, len2);

																	for(int k = 0; k < n; k++)
																	{
																		char ch1 = s1.charAt(k);
																		char ch2 = s2.charAt(k);

																		if(ch1 != ch2)
																		{
																			if(Character.isUpperCase(ch1) != Character
																				.isUpperCase(ch2))
																				return ch2 - ch1;
																			else
																				return ch1 - ch2;
																		}
																	}

																	final int result = len1 - len2;

																	if(result != 0)
																		return result;

																	switch(SortBy.this)
																	{
																		case METHOD:
																			return NAME.comparator.compare(o1, o2);
																		default:
																			return 0;
																	}
																}
															};

		@SuppressWarnings("rawtypes")
		private Comparable getComparableValueOf(MethodStat stat)
		{
			switch(this)
			{
				case AVG:
					return stat.total / stat.count;
				case COUNT:
					return stat.count;
				case TOTAL:
					return stat.total;
				case NAME:
					return stat.className;
				case METHOD:
					return stat.methodName;
				case MIN:
					return stat.min;
				case MAX:
					return stat.max;
				default:
					throw new InternalError();
			}
		}

		private static final SortBy[]	VALUES	= SortBy.values();
	}

	public static void dumpClassStats()
	{
		dumpClassStats(null);
	}

	@SuppressWarnings("rawtypes")
	public static void dumpClassStats(final SortBy sortBy)
	{
		final List<MethodStat> methodStats = new ArrayList<MethodStat>();

		synchronized(RunnableStatsManager.class)
		{
			for(ClassStat classStat : classStats.values())
				for(MethodStat methodStat : classStat.methodStats)
					if(methodStat.count > 0)
						methodStats.add(methodStat);
		}

		if(sortBy != null)
			Collections.sort(methodStats, sortBy.comparator);

		final List<String> lines = new ArrayList<String>();

		lines.add("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>");
		lines.add("<entries>");
		lines.add("\t<!-- This XML contains statistics about execution times. -->");
		lines.add("\t<!-- Submitted results will help the developers to optimize the server. -->");

		final String[][] values = new String[SortBy.VALUES.length][methodStats.size()];
		final int[] maxLength = new int[SortBy.VALUES.length];

		for(int i = 0; i < SortBy.VALUES.length; i++)
		{
			final SortBy sort = SortBy.VALUES[i];

			for(int k = 0; k < methodStats.size(); k++)
			{
				final Comparable c = sort.getComparableValueOf(methodStats.get(k));

				final String value;

				if(c instanceof Number)
					value = NumberFormat.getInstance(Locale.ENGLISH).format(((Number) c).longValue());
				else
					value = String.valueOf(c);

				values[i][k] = value;

				maxLength[i] = Math.max(maxLength[i], value.length());
			}
		}

		for(int k = 0; k < methodStats.size(); k++)
		{
			TextBuilder tb = TextBuilder.newInstance();
			tb.append("\t<entry ");

			EnumSet<SortBy> set = EnumSet.allOf(SortBy.class);

			if(sortBy != null)
			{
				switch(sortBy)
				{
					case NAME:
					case METHOD:
						appendAttribute(tb, SortBy.NAME, values[SortBy.NAME.ordinal()][k], maxLength[SortBy.NAME
							.ordinal()]);
						set.remove(SortBy.NAME);

						appendAttribute(tb, SortBy.METHOD, values[SortBy.METHOD.ordinal()][k], maxLength[SortBy.METHOD
							.ordinal()]);
						set.remove(SortBy.METHOD);
						break;
					default:
						appendAttribute(tb, sortBy, values[sortBy.ordinal()][k], maxLength[sortBy.ordinal()]);
						set.remove(sortBy);
						break;
				}
			}

			for(SortBy sort : SortBy.VALUES)
				if(set.contains(sort))
					appendAttribute(tb, sort, values[sort.ordinal()][k], maxLength[sort.ordinal()]);

			tb.append("/>");

			lines.add(tb.toString());
			TextBuilder.recycle(tb);
		}

		lines.add("</entries>");

		PrintStream ps = null;
		try
		{
			ps = new PrintStream("log/MethodStats-" + System.currentTimeMillis() + ".log");

			for(String line : lines)
				ps.println(line);
		}
		catch(Exception e)
		{
			log.warn("", e);
		}
		finally
		{
			IOUtils.closeQuietly(ps);
		}
	}

	private static void appendAttribute(TextBuilder tb, SortBy sortBy, String value, int fillTo)
	{
		tb.append(sortBy.xmlAttributeName);
		tb.append("=");

		if(sortBy != SortBy.NAME && sortBy != SortBy.METHOD)
			for(int i = value.length(); i < fillTo; i++)
				tb.append(" ");

		tb.append("\"");
		tb.append(value);
		tb.append("\" ");

		if(sortBy == SortBy.NAME || sortBy == SortBy.METHOD)
			for(int i = value.length(); i < fillTo; i++)
				tb.append(" ");
	}
}